/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package bgu.sonar.util.evt;

import bgu.sonar.util.ex.UncheckedInterruptedException;
import com.esotericsoftware.reflectasm.MethodAccess;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.swing.SwingUtilities;
import org.openide.util.Exceptions;

/**
 *
 * @author bennyl
 */
public class GlobalEvent {

    private static ConcurrentHashMap<Class<? extends GlobalEvent>, ConcurrentLinkedQueue<Subscriber>> subscribers = new ConcurrentHashMap<>();

    private static List<Class<? extends GlobalEvent>> allHandledEvents(Class of) {
        List<Class<? extends GlobalEvent>> result = new LinkedList<>();
        for (Method m : of.getMethods()) {
            if (Modifier.isPublic(m.getModifiers()) && m.getName().equals("onEvent")) {
                Class[] ptypes = m.getParameterTypes();
                if (ptypes.length == 1 && GlobalEvent.class.isAssignableFrom(ptypes[0])) {
                    result.add((Class<? extends GlobalEvent>) m.getParameterTypes()[0]);
                }
            }
        }

        return result;
    }

    public static void subscribeWeakly(Object subscriber) {
        for (Class<? extends GlobalEvent> to : allHandledEvents(subscriber.getClass())) {
            lookupSubscribersList(to).add(new WeakSubscriber(subscriber, to));
        }
    }

    public static void subscribeStrongly(Object subscriber) {
        for (Class<? extends GlobalEvent> to : allHandledEvents(subscriber.getClass())) {
            lookupSubscribersList(to).add(new StrongSubscriber(subscriber, to));
        }
    }

    public static void unsubscribe(Object subscriber) {
        for (Class<? extends GlobalEvent> from : allHandledEvents(subscriber.getClass())) {
            for (Iterator<Subscriber> it = subscribers.get(from).iterator(); it.hasNext();) {
                Subscriber s = it.next();
                if (s == null) {
                    it.remove();
                } else {
                    if (s.equals(subscriber)) {
                        it.remove();
                        return;
                    }
                }
            }
        }
    }

    public static void post(GlobalEvent e) {
        Class<? extends GlobalEvent> ecls = e.getClass();
        final ConcurrentLinkedQueue<Subscriber> subs = subscribers.get(ecls);
        if (subs == null) {
            return;
        }
        for (Iterator<Subscriber> it = subs.iterator(); it.hasNext();) {
            Subscriber s = it.next();
            if (s == null) {
                it.remove();
            } else {
                s.handle(e);
            }
        }

    }

    public void post() {
        post(this);
    }

    /**
     * post in swing event dispatcher thread
     */
    public void postInEDT() {
        if (SwingUtilities.isEventDispatchThread()) {
            post(this);
        } else {
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        post(GlobalEvent.this);
                    }
                });
            } catch (InterruptedException ex) {
                throw new UncheckedInterruptedException(ex);
            } catch (InvocationTargetException ex) {
                throw new RuntimeException(ex.getCause());
            }
        }
    }

    private static ConcurrentLinkedQueue<Subscriber> lookupSubscribersList(Class<? extends GlobalEvent> to) {
        ConcurrentLinkedQueue<Subscriber> list = subscribers.get(to);
        if (list == null) {
            list = new ConcurrentLinkedQueue<>();
            subscribers.putIfAbsent(to, list);
            list = subscribers.get(to);
        }
        return list;
    }

    private static abstract class Subscriber {

        MethodAccess ma;
        int midx;

        public Subscriber(Object to, Class eventClass) {
            ma = MethodAccess.get(to.getClass());
            midx = ma.getIndex("onEvent", eventClass);
        }

        public boolean handle(GlobalEvent e) {
            Object got = getSubscriber();
            if (got == null) {
                return false;
            } else {
                ma.invoke(got, midx, e);
                return true;
            }
        }

        public abstract Object getSubscriber();
    }

    private static class WeakSubscriber extends Subscriber {

        WeakReference reference;

        public WeakSubscriber(Object to, Class eventClass) {
            super(to, eventClass);
            this.reference = new WeakReference(to);
        }

        @Override
        public Object getSubscriber() {
            return reference.get();
        }
    }

    private static class StrongSubscriber extends Subscriber {

        Object subs;

        public StrongSubscriber(Object to, Class eventClass) {
            super(to, eventClass);
            subs = to;
        }

        @Override
        public Object getSubscriber() {
            return subs;
        }
    }
}
